/**
 * This file is Copyright © 2008 Software Craftsmen Limited. All Rights Reserved.
 */
package com.softwarecraftsmen.orogeny.filing;

import com.softwarecraftsmen.inputStreamReaders.InputStreamReader;
import com.softwarecraftsmen.inputStreamReaders.InputStreamReader.Nothing;
import com.softwarecraftsmen.inputStreamReaders.ReadableData;
import com.softwarecraftsmen.orogeny.filing.fileChannelReaders.FileChannelReader;
import com.softwarecraftsmen.orogeny.filing.fileChannelWriters.FileChannelWriter;
import com.softwarecraftsmen.orogeny.filing.findFileFilters.FindFilesFilter;
import com.softwarecraftsmen.orogeny.filing.findSubdirectoriesFilters.FindSubdirectoriesFilter;
import com.softwarecraftsmen.outputStreamWriters.OutputStreamWriter;
import com.softwarecraftsmen.outputStreamWriters.WritableData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;

import static com.softwarecraftsmen.orogeny.filing.AbsoluteDirectoriesAndFiles.absoluteDirectoriesAndFiles;
import static com.softwarecraftsmen.orogeny.filing.AbsoluteDirectory.absoluteDirectory;
import static com.softwarecraftsmen.orogeny.filing.AbsoluteFiles.absoluteFiles;
import static com.softwarecraftsmen.orogeny.filing.RelativeDirectory.relativeDirectory;
import static com.softwarecraftsmen.orogeny.filing.fileSystems.AbstractFileSystem.CurrentFileSystem;
import static com.softwarecraftsmen.orogeny.operatingSystems.OperatingSystemFamily.currentOperatingSystemFamily;
import static java.io.File.createTempFile;
import static java.nio.file.Files.*;
import static java.nio.file.Paths.get;
import static java.util.Collections.emptySet;

public class AbsoluteFile extends AbstractAbsolutePath implements ReadableData, WritableData, Execute
{
	@Nullable
	public static final AbsoluteFile SHELL = safeAbsoluteFileFromEnvironmentVariable("SHELL", null);

	@Nullable
	public static final AbsoluteFile BASH = safeAbsoluteFileFromEnvironmentVariable("BASH", null);

	@Nullable
	private static AbsoluteFile safeAbsoluteFileFromEnvironmentVariable(final @NotNull String environmentVariableNameCaseSensitivityHandled, final @Nullable AbsoluteFile defaultValue)
	{
		AbsoluteFile result;
		try
		{
			result = absoluteFileFromEnvironmentVariable(environmentVariableNameCaseSensitivityHandled, defaultValue);
		}
		catch (Exception exception)
		{
			result = defaultValue;
		}
		return result;
	}

	@Nullable
	public static AbsoluteFile absoluteFileFromEnvironmentVariable(final @NotNull String environmentVariableNameCaseSensitivityHandled, final @Nullable AbsoluteFile defaultValue)
	{
		final String path = currentOperatingSystemFamily().getEnvironmentVariableWithCaseSensitivityPerPlatform(environmentVariableNameCaseSensitivityHandled);
		if (path == null)
		{
			return defaultValue;
		}
		else
		{
			return CurrentFileSystem.toFile(CurrentFileSystem.root(), path);
		}
	}

	private final AbsoluteDirectory directory;
	private final FileName fileName;
	private static final Set<AbsoluteDirectory> EmptyAbsoluteDirectories = emptySet();
	private static final Set<AbsoluteFile> EmptyAbsoluteFile = emptySet();
	private static final int BufferSize = 1024 * 1024 * 10;

	public AbsoluteFile(final @NotNull AbsoluteDirectory directory, final @NotNull FileName fileName)
	{
		this.directory = directory;
		this.fileName = fileName;
	}

	@NotNull
	public String toString()
	{
		return toFileSystemSpecificPath();
	}

	public boolean equals(final @Nullable Object o)
	{
		if (this == o)
		{
			return true;
		}
		if (o == null || getClass() != o.getClass())
		{
			return false;
		}

		final AbsoluteFile that = (AbsoluteFile) o;
		return directory.equals(that.directory) && fileName.equals(that.fileName);
	}

	public int hashCode()
	{
		int result;
		result = directory.hashCode();
		result = 31 * result + fileName.hashCode();
		return result;
	}

	@NotNull
	public AbsolutePaths and(final @NotNull AbsolutePath absolutePath)
	{
		return absoluteDirectoriesAndFiles(this, absolutePath);
	}

	@NotNull
	public AbsoluteDirectoriesAndFiles and(final @NotNull AbsoluteDirectory absoluteDirectory)
	{
		return absoluteDirectoriesAndFiles(this, absoluteDirectory);
	}

	@NotNull
	public AbsoluteFiles and(final @NotNull AbsoluteFile absoluteFile)
	{
		return absoluteFiles(this, absoluteFile);
	}

	@NotNull
	public AbsolutePaths and(final @NotNull AbsolutePaths absolutePaths)
	{
		return absolutePaths.and(this);
	}

	@NotNull
	public AbsoluteDirectoriesAndFiles and(final @NotNull AbsoluteDirectories absoluteDirectories)
	{
		return absoluteDirectories.and(this);
	}

	@NotNull
	public AbsoluteFiles and(final @NotNull AbsoluteFiles absoluteFiles)
	{
		return absoluteFiles.and(this);
	}

	@NotNull
	public AbsoluteDirectoriesAndFiles and(final @NotNull AbsoluteDirectoriesAndFiles absoluteDirectoriesAndFiles)
	{
		return absoluteDirectoriesAndFiles.and(this);
	}

	@NotNull
	public File toFile()
	{
		return fileName.toFile(directory);
	}

	@NotNull
	public String toFileSystemSpecificPath()
	{
		return fileName.toFileSystemSpecificPath(directory);
	}

	@NotNull
	public String toFileSystemSpecificPathPrefixedByPathSeparator()
	{
		return fileName.toFileSystemSpecificPathPrefixedByPathSeparator(directory);
	}

	@NotNull
	public String toFileSystemSpecificPathPostfixedByPathSeparator()
	{
		return fileName.toFileSystemSpecificPathPostfixedByPathSeparator(directory);
	}

	@NotNull
	public RelativeDirectory relativeDirectoryToRootGiven(final @NotNull AbsoluteDirectory root)
	{
		return root.relativeDirectoryAssumingAbsoluteDirectoryGivenIsSubdirectory(directory);
	}

	@NotNull
	public Set<AbsoluteFile> findFiles(final @NotNull FindFilesFilter findFilesFilter)
	{
		if (findFilesFilter.include(directory, relativeDirectory(), fileName))
		{
			return new LinkedHashSet<AbsoluteFile>()
			{{
				add(AbsoluteFile.this);
			}};
		}
		return EmptyAbsoluteFile;
	}

	@NotNull
	public Set<AbsoluteDirectory> findSubdirectories(final @NotNull FindSubdirectoriesFilter findSubdirectoriesFilter)
	{
		return EmptyAbsoluteDirectories;
	}

	@NotNull
	@SuppressWarnings({"ThrowFromFinallyBlock"})
	public <T> T readData(final @NotNull InputStreamReader<T> inputStreamReader) throws IOException
	{
		return internalReadData(new InputStreamReader<T>()
		{
			@NotNull
			public T read(final int inputBufferSize, final @NotNull BufferedInputStream inputStream) throws IOException
			{
				return inputStreamReader.read(inputBufferSize, inputStream);
			}
		});
	}

	@NotNull
	@SuppressWarnings({"ThrowFromFinallyBlock"})
	public <T> T readData(final @NotNull FileChannelReader<T> fileChannelReader) throws IOException
	{
		return internalReadData(new InputStreamReader<T>()
		{
			@NotNull
			public T read(final int inputBufferSize, final @NotNull BufferedInputStream inputStream) throws IOException
			{
				return fileChannelReader.read(((FileChannelBackedStream)inputStream).backingFileChannel());
			}
		});
	}

	@NotNull
	@SuppressWarnings({"ThrowFromFinallyBlock"})
	private <T> T internalReadData(final @NotNull InputStreamReader<T> inputStreamReader) throws IOException
	{
		final BufferedFileInputStream inputStream = new BufferedFileInputStream(new FileInputStream(toFile()), BufferSize);
		boolean exceptionThrown = true;
		try
		{
			final T t = inputStreamReader.read(BufferSize, inputStream);
			exceptionThrown = false;
			return t;
		}
		finally
		{
			try
			{
				inputStream.close();
			}
			catch (IOException e)
			{
				if (!exceptionThrown)
				{
					throw e;
				}
			}
		}
	}

	@SuppressWarnings({"ThrowFromFinallyBlock"})
	public void writeData(final @NotNull OutputStreamWriter outputStreamWriter) throws IOException, CouldNotMakeAllFoldersNecessaryException, IsNotADirectoryException
	{
		internalWriteData(new OutputStreamWriter()
		{
			public void write(final int outputBufferSize, final @NotNull BufferedOutputStream outputStream) throws IOException
			{
				outputStreamWriter.write(outputBufferSize, outputStream);
			}
		});
	}

	@SuppressWarnings({"ThrowFromFinallyBlock"})
	public void writeData(final @NotNull FileChannelWriter fileChannelWriter) throws IOException, CouldNotMakeAllFoldersNecessaryException, IsNotADirectoryException
	{
		internalWriteData(new OutputStreamWriter()
		{
			public void write(final int outputBufferSize, final @NotNull BufferedOutputStream outputStream) throws IOException
			{
				fileChannelWriter.write(((FileChannelBackedStream) outputStream).backingFileChannel());
			}
		});
	}

	@SuppressWarnings({"ThrowFromFinallyBlock"})
	private void internalWriteData(final @NotNull OutputStreamWriter outputStreamWriter) throws IOException, CouldNotMakeAllFoldersNecessaryException, IsNotADirectoryException
	{
		directory.makeAllFoldersNecessary();
		final BufferedFileOutputStream outputStream = new BufferedFileOutputStream(new FileOutputStream(toFile()), BufferSize);
		boolean tryFailed = true;
		try
		{
			outputStreamWriter.write(BufferSize, outputStream);
			tryFailed = false;
		}
		finally
		{
			try
			{
				outputStream.close();
			}
			catch (IOException e)
			{
				if (!tryFailed)
				{
					throw e;
				}
			}
		}
	}

    public boolean exists()
	{
		return fileName.exists(directory);
	}

	@NotNull
	public RelativeFile toRelativeFile(final @NotNull AbsoluteDirectory commonRoot)
	{
		final RelativeDirectory relativeDirectory = commonRoot.relativeDirectoryAssumingAbsoluteFileGivenIsSubdirectory(this);
		return new RelativeFile(relativeDirectory, fileName);
	}

	public void hardLinkTo(final @NotNull AbsoluteFile copy) throws IOException
	{
		if (!exists())
		{
			throw new IsNotAFileException(directory, fileName);
		}
		if (copy.exists())
		{
			throw new AlreadyExistsException(copy);
		}
		if (equals(copy))
		{
			return;
		}

		final String fromPath = this.toFileSystemSpecificPath();
		final String destinationPath = copy.toFileSystemSpecificPath();

		final Path fromFilePath = get(fromPath);
		if (isSymbolicLink(fromFilePath))
		{
			final Path pathPointedTo = readSymbolicLink(get(fromPath));
			createSymbolicLink(get(destinationPath), pathPointedTo);
			return;
		}

		if (!isRegularFile(fromFilePath))
		{
			throw new IsNotAFileException(directory, fileName);
		}

		try
		{
			Files.createLink(get(destinationPath), fromFilePath);
		}
		catch (IOException | UnsupportedOperationException ignored)
		{
			readData(new FileChannelReader<Nothing>()
			{
				@NotNull
				public Nothing read(final @NotNull FileChannel readableFileChannel) throws IOException
				{
					copy.writeData(new FileChannelWriter()
					{
						public void write(final @NotNull FileChannel writableFileChannel) throws IOException
						{
							writableFileChannel.transferFrom(readableFileChannel, 0, readableFileChannel.size());
						}
					});
					return InputStreamReader.Nothing;
				}
			});
		}
	}

	public void moveTo(final @NotNull AbsoluteFile destination)
	{
		try
		{
			hardLinkTo(destination);
			delete();
		}
		catch (IOException e)
		{
			throw new CouldNotMoveException(this, destination, e);
		}
		catch (@SuppressWarnings("TryWithIdenticalCatches") FilingOperationFailedException e)
		{
			throw new CouldNotMoveException(this, destination, e);
		}
	}

	@NotNull
	public AbsoluteFile moveToTimestampedName()
	{
		final FileName timeStampedName = fileName.timestampedName();
		final AbsoluteFile destination = new AbsoluteFile(directory, timeStampedName);
		moveTo(destination);
		return destination;
	}

	public void delete()
	{
		fileName.delete(directory);
	}

	@NotNull
	public AbsoluteFile asIfIn(final @NotNull AbsoluteDirectory toDirectory)
	{
		return new AbsoluteFile(toDirectory, fileName);
	}

	@NotNull
	public static AbsoluteFile createTemporaryFile(final @NotNull String suffixExcludingDot) throws IOException
	{
		final File tempFile = createTempFile("com.softwarecraftsmen.orogeny.filing.", "." + suffixExcludingDot);
		tempFile.deleteOnExit();
		return absoluteDirectory(CurrentFileSystem, tempFile.getParentFile()).file(tempFile.getName());
	}

	@NotNull
	public ZipFile openAsZipFile() throws NotAZipFileException
	{
		try
		{
			return new ZipFile(toFile());
		}
		catch (IOException e)
		{
			throw new NotAZipFileException(this);
		}
	}

	@NotNull
	public JarFile openAsJarFile() throws NotAZipFileException
	{
		try
		{
			return new JarFile(toFile());
		}
		catch (IOException e)
		{
			throw new NotAZipFileException(this);
		}
	}

	public void setExecutable(final @NotNull ProcessBuilder processBuilder)
	{
		processBuilder.command(toFileSystemSpecificPath());
	}

	public boolean isExecutable()
	{
		return exists() && toFile().canExecute();
	}

	@NotNull
	public FileName fileName()
	{
		return fileName;
	}

	@NotNull
	public AbsoluteDirectory parentDirectory()
	{
		return directory;
	}

	private interface FileChannelBackedStream
	{
		@NotNull
		FileChannel backingFileChannel();
	}

	private static final class BufferedFileInputStream extends BufferedInputStream implements FileChannelBackedStream
	{
		public BufferedFileInputStream(final @NotNull FileInputStream inputStream, final int bufferSize)
		{
			super(inputStream, bufferSize);
		}

		@NotNull
		public FileChannel backingFileChannel()
		{
			return ((FileInputStream) super.in).getChannel();
		}
	}

	private static final class BufferedFileOutputStream extends BufferedOutputStream implements FileChannelBackedStream
	{
		public BufferedFileOutputStream(final @NotNull FileOutputStream inputStream, final int bufferSize)
		{
			super(inputStream, bufferSize);
		}

		@NotNull
		public FileChannel backingFileChannel()
		{
			return ((FileOutputStream) super.out).getChannel();
		}
	}
}
